import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc'; // 用于处理UTC时间
import timezone from 'dayjs/plugin/timezone'; // 用于处理时区
import { EntityManager } from "typeorm";
import { errLogger, outLogger } from "../../common/logger";
import { entitysMap, tableNameFactory, KLineDTO, createEntitySchema } from './KLineHistory.entity'
import { omit } from 'lodash';
import { binanceCommon } from '../../binance/BinanceCommon';
import { MysqlDataSource } from '../../db/orm';
import { Period } from "../../trader/PeriodProcess";
import { getDayRange, roundTimeToInterval, PERIOD_INTERVAL_MAP } from '../../utils';
import { AsyncQueue } from 'ROOT/common/AsyncQueue';

dayjs.extend(utc);
dayjs.extend(timezone);
// 设置默认时区
dayjs.tz.setDefault("Asia/Shanghai");

class KLineHistoryService {

  constructor() {
    //
  }

  async find({ start, end, symbol }: { start: string | number | Date | dayjs.Dayjs; end: string | number | Date | dayjs.Dayjs; symbol: string }): Promise<KLineDTO[]> {
    const startYear = dayjs(start).year();
    const endYear = dayjs(end).year();
    const yearDiff = endYear - startYear + 1;
  
    const query = [];
    for (let i = 0; i < yearDiff; i++) {
      const year = startYear + i;
      let yearLastDay = dayjs(`${year}-12-31`);
      yearLastDay = yearLastDay.hour(23);
      yearLastDay = yearLastDay.minute(59);
  
      let endDay = dayjs.tz(end, "Asia/Shanghai");
      // endDay = endDay.hour(23);
      // endDay = endDay.minute(59);

      const yearStart = i === 0 ? dayjs(start) : dayjs(`${year}-01-01`);
      const yearEnd = i === yearDiff - 1 ? dayjs(endDay) : yearLastDay;
    
      const subQuery = `(SELECT * FROM ${tableNameFactory.getTableName(year)}
          WHERE time BETWEEN '${yearStart.format('YYYY-MM-DD HH:mm:ss')}' AND '${yearEnd.format('YYYY-MM-DD HH:mm:ss')}'
          AND symbol='${symbol.toUpperCase()}')`;

      query.push(subQuery);
    }

    const res = await MysqlDataSource.getRepository(entitysMap[new Date().getFullYear()])
      .query(query.join('UNION ALL\n') + 'ORDER BY time ASC')

    return res;
  }

  async update(data: Omit<KLineDTO, 'id'>): Promise<KLineDTO> {
    // request data
    const repository = MysqlDataSource.getRepository(entitysMap[data.time.getFullYear()])
    const target = await repository.findOne({ where: { symbol: data.symbol.toUpperCase(), time: data.time } })
  
    if (!target) {
      return await repository.save(data);
    }
    Object.assign(target, data)
    return await repository.save(target);
  }

  async create(data: Omit<KLineDTO, 'id'>): Promise<KLineDTO> {
    // request data
    const repository = MysqlDataSource.getRepository(entitysMap[data.time.getFullYear()])
    const target = await repository.findOne({ where: { symbol: data.symbol.toUpperCase(), time: data.time } })
    if (target == null) {
      return await repository.save(data);
    }
    Object.assign(target, data)
    return await repository.save(target);
  }

  async batchUpdate(updateDataList: (KLineDTO & { id: string })[]): Promise<void> {
    const errResult = []
    // 事务跨表更新 
    await MysqlDataSource.transaction(async (transactionalEntityManager: EntityManager) => {
      for (let i = 0; i < updateDataList.length; i++) {
        const year = dayjs(updateDataList[i].time).format('YYYY');
        const tableName = tableNameFactory.getTableName(year);
        const repository = transactionalEntityManager.getRepository(createEntitySchema(year))
        // 批量更新数据 
        const data = await repository.createQueryBuilder(tableName)
          .update(createEntitySchema(year))
          .set(omit(updateDataList[i], ['id', 'time', 'symbol']))
          .where(`${tableName}.id = :id`, { id: updateDataList[i].id })
          .andWhere(`${tableName}.symbol = :symbol`, { symbol: updateDataList[i].symbol })
          .execute()
        if (!data.affected) {
          return errResult.push(data);
        }
      }
    })
    if (errResult.length) {
      throw Error(`${errResult.length}条批量更新失败`)
    }
  }

  async fillMissData(symbol: string, startDate: Date, endDate: Date, period: Period): Promise<void> {
    const taskTimes = getDayRange(startDate, endDate, 2);
    const interval = PERIOD_INTERVAL_MAP[period];
    const tradeHistory = await this.find({ start: startDate.getTime() - interval, end: endDate.getTime() + interval, symbol });

    while (taskTimes.length) {
      const [start, end] = taskTimes.shift();
      // 对起始时间和结束时间向下取整，以对齐指定周期的边界
      const roundedStartTime = roundTimeToInterval(period, start.getTime());
      const roundedEndTime = roundTimeToInterval(period, end.getTime());
      const missDate: number[] = [];

      for (let currentTimestamp = roundedStartTime; currentTimestamp < roundedEndTime; currentTimestamp += interval) {
        const currentData = tradeHistory.find((data) => data.time.getTime() === currentTimestamp);
        if (!currentData || currentData.close === 0 || currentData.low === 0) {
          missDate.push(currentTimestamp);
        }
      }
      if (missDate[0] === undefined) {
        continue;
      }
      const len = missDate.length;
      if (missDate.length == 1) {
        missDate.push(missDate[0] + interval);
        missDate[0] = missDate[0] - interval;
      } else {
        missDate[missDate.length - 1] = missDate[missDate.length - 1] + interval;
      }
      outLogger.info(`${symbol}: missDate(${len}):`, dayjs(missDate[0]).toDate(), dayjs(missDate[1]).toDate());
      const startTime = missDate[0];
      const endTime = missDate[missDate.length - 1];
      try {
        const candles = await binanceCommon.getCandles({ symbol, startTime: startTime, endTime: endTime, interval: period, limit: 500 });
        if (candles.length) {
          outLogger.info('getCandles', symbol, dayjs(candles[0].openTime).toDate(), candles.length);
          const asyncQueue = new AsyncQueue(10);
          for (let i = 0; i < candles.length; i++) {
            asyncQueue.enqueue(() => {
              const data = candles[i];
              const newData = {
                symbol: symbol,
                close: Number(data.close),
                high: Number(data.high),
                low: Number(data.low),
                open: Number(data.open),
                time: dayjs(data.openTime).toDate(),
                volume: Number(data.volume),
                buy: Number(data.takerBuyBaseAssetVolume),
                sell: Number(data.volume) - Number(data.takerBuyBaseAssetVolume),
              }
              return this.update(newData)
            });
          }
          await asyncQueue.awaitAll();
        }
        await new Promise((resolve) => setTimeout(resolve, 200));
      } catch (error) {
        outLogger.error(error.message)
      }
    }
  }

  async syncKLineData(symbol: string, startDate: Date, endDate: Date, period: Period): Promise<void> {
    const taskTimes = getDayRange(startDate, endDate, 10);
    outLogger.info(`${symbol} taskTimes(${taskTimes.length}): ${taskTimes[0].toString()} <-> ${taskTimes[taskTimes.length - 1].toString()}`);
    while (taskTimes.length) {
      const [start, end] = taskTimes.shift();
      outLogger.info(`${symbol} range`, dayjs(start).toDate(), dayjs(end).toDate());
      const startTime = start.getTime();
      const endTime = end.getTime();
      try {
        const candles = await binanceCommon.getCandles({ symbol, startTime: startTime, endTime: endTime, interval: period, limit: 1000 });
        if (candles.length) {
          const asyncQueue = new AsyncQueue(10);
          for (let i = 0; i < candles.length; i++) {
            asyncQueue.enqueue(() => {
              const data = candles[i];
              const newData = {
                symbol: symbol,
                close: Number(data.close),
                high: Number(data.high),
                low: Number(data.low),
                open: Number(data.open),
                time: dayjs(data.openTime).toDate(),
                volume: Number(data.volume),
                buy: Number(data.takerBuyBaseAssetVolume),
                sell: Number(data.volume) - Number(data.takerBuyBaseAssetVolume),
              }
              return this.create(newData);
            });
          }
          await asyncQueue.awaitAll();
        }
        await new Promise((resolve) => setTimeout(resolve, 300));
      } catch (error) {
        outLogger.error('syncKLineData', error.message)
      }
    }
    outLogger.info(`${symbol}`, 'end');
  }

  async findEarliestDataMonth(symbol: string, startTime: string | dayjs.Dayjs) {
    let startYear = dayjs(startTime).year();
    let startMonth = dayjs(startTime).month();
    let endYear = dayjs().year();
    let endMonth = dayjs().month();
  
    // Binary search
    while (startYear < endYear || (startYear === endYear && startMonth <= endMonth)) {
      const midMonth = (startYear * 12 + startMonth + endYear * 12 + endMonth) >> 1;
      const midMonthYear = Math.floor(midMonth / 12);
      const midMonthMonth = midMonth % 12;
  
      const midMonthStart = dayjs(startTime).set('year', midMonthYear).set('month', midMonthMonth).startOf('month');
      const midMonthEnd = midMonthStart.add(1, 'month').startOf('month');
      
      const candles = await binanceCommon.getCandles({
        symbol,
        startTime: midMonthStart.valueOf(),
        endTime: midMonthEnd.valueOf(),
        interval: '15m',
        limit: 100
      });

      if (candles && candles.length > 0) {
        // If there is data, narrow the search to the left
        endYear = midMonthYear;
        endMonth = midMonthMonth - 1;
      } else {
        // If there is no data, narrow the search to the right
        startYear = midMonthYear;
        startMonth = midMonthMonth + 1;
      }
    }
    // Return the found earliest month with data
    const startDate = dayjs();
    return startDate.set('year', startYear).set('month', startMonth);
  }
}

export default new KLineHistoryService();


// outLogger.info(`new Date('2022-12-31T18:10:19.691Z').getFullYear()`, new Date('2022-12-31T18:10:19.691Z').getFullYear(), new Date(1672510219691).getFullYear());
